﻿@page "/pages/authentication/externallogin"
@using System.Security.Claims
@using System.Text
@using CleanArchitecture.Blazor.Application.Common.Interfaces.MultiTenant
@using CleanArchitecture.Blazor.Domain.Identity
@using CleanArchitecture.Blazor.Infrastructure.Constants.Localization
@using CleanArchitecture.Blazor.Infrastructure.Constants.Role
@using Microsoft.AspNetCore.WebUtilities
@using CleanArchitecture.Blazor.Application.Features.Identity.Notifications.UserActivation
@using System.ComponentModel.DataAnnotations
@using System.Globalization

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger
@inject IStringLocalizer<ExternalLogin> L
@inject ITenantService TenantsService
<PageTitle>@L["External Login"]</PageTitle>

<MudText Typo="Typo.h4" GutterBottom="true">@L["External Login"]</MudText>
<MudText Typo="Typo.h6" GutterBottom="true">@string.Format(L["Associate your {0} account."], ProviderDisplayName)</MudText>

<MudText Typo="Typo.body1"> @string.Format(L["You've successfully authenticated with {0}. Please enter an email address for this site below and click the Register button to finish logging in."], ProviderDisplayName)</MudText>


<div class="d-flex flex-column gap-y-3">
    <div class="d-flex flex-column">
        <EditForm Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
            <DataAnnotationsValidator/>
            <StatusMessage Message="@message" Error="true"/>
            <div class="mud-input-control mud-input-input-control my-4">
                <div class="mud-input-control-input-container">
                    <div class="mud-input mud-input-outlined mud-shrink">
                        <InputSelect @bind-Value="Input.TenantId" class="mud-input-slot mud-input-root mud-input-root-outlined" style="padding: 18.5px 14px;
                                                                                                                                       width:100%;
                                                                                                                                       appearance: none;
                                                                                                                                       background: url('') no-repeat right 10px center;
                                                                                                                                        background-size: 10px;" aria-required="true">
                            @foreach (var tenant in TenantsService.DataSource)
                            {
                                <option value="@tenant.Id">@tenant.Name</option>
                            }
                        </InputSelect>
                        <div class="mud-input-outlined-border"></div>
                    </div>
                    <label class="mud-input-label mud-input-label-animated mud-input-label-outlined mud-input-label-inputcontrol" for="TenantId">@L["Tenant"]</label>
                </div>
                <div class="mud-input-helper-text mud-input-error">
                    <div class="d-flex">
                        <ValidationMessage For="() => Input.TenantId" class="mud-input-error" />
                    </div>
                </div>
            </div>
            <div class="mud-input-control mud-input-input-control my-4">
                <div class="mud-input-control-input-container">
                    <div class="mud-input mud-input-outlined mud-shrink">
                        <InputText type="text" @bind-Value="Input.Email" class="mud-input-slot mud-input-root mud-input-root-outlined" aria-required="true" autocomplete="email" placeholder="Please enter your email."/>
                        <div class="mud-input-slot mud-input-root mud-input-root-outlined" style="display:none"></div>
                        <div class="mud-input-outlined-border"></div>
                    </div>
                    <label class="mud-input-label mud-input-label-animated mud-input-label-outlined mud-input-label-inputcontrol" for="email">@L["Email"]</label>
                </div>
                <div class="mud-input-helper-text mud-input-error">
                    <div class="d-flex">
                        <ValidationMessage For="() => Input.Email" class="mud-input-error"/>
                    </div>
                </div>
            </div>
            <div class="mud-input-control mud-input-input-control my-4">
                <div class="mud-input-control-input-container">
                    <div class="mud-input mud-input-outlined mud-shrink">
                        <InputSelect @bind-Value="Input.TimeZoneId" class="mud-input-slot mud-input-root mud-input-root-outlined" style="padding: 18.5px 14px;
                                                                                                                                       width:100%; 
                                                                                                                                       appearance: none;
                                                                                                                                       background: url('') no-repeat right 10px center;
                                                                                                                                       background-size: 10px;" aria-required="true">
                            @foreach (var item in TimeZoneInfo.GetSystemTimeZones().ToList())
                            {
                                <option value="@item.Id">@item.DisplayName</option>
                            }
                        </InputSelect>
                        <div class="mud-input-outlined-border"></div>
                    </div>
                    <label class="mud-input-label mud-input-label-animated mud-input-label-outlined mud-input-label-inputcontrol" for="TenantId">@L["Time Zone"]</label>
                </div>
                <div class="mud-input-helper-text mud-input-error">
                    <div class="d-flex">
                        <ValidationMessage For="() => Input.TimeZoneId" class="mud-input-error" />
                    </div>
                </div>
            </div>
            <div class="mud-input-control mud-input-input-control my-4">
                <div class="mud-input-control-input-container">
                    <div class="mud-input mud-input-outlined mud-shrink">
                        <InputSelect @bind-Value="Input.LanguageCode" class="mud-input-slot mud-input-root mud-input-root-outlined" style="padding: 18.5px 14px;
                                                                                                                                       width:100%; 
                                                                                                                                       appearance: none;
                                                                                                                                       background: url('') no-repeat right 10px center;
                                                                                                                                       background-size: 10px;" aria-required="true">
                            @foreach (var item in LocalizationConstants.SupportedLanguages.ToList())
                            {
                                <option value="@item.Code">@item.DisplayName</option>
                            }
                        </InputSelect>
                        <div class="mud-input-outlined-border"></div>
                    </div>
                    <label class="mud-input-label mud-input-label-animated mud-input-label-outlined mud-input-label-inputcontrol" for="TenantId">@L["Language"]</label>
                </div>
                <div class="mud-input-helper-text mud-input-error">
                    <div class="d-flex">
                        <ValidationMessage For="() => Input.LanguageCode" class="mud-input-error" />
                    </div>
                </div>
            </div>
            <MudButton Variant="Variant.Filled"
                       Color="Color.Primary"
                       Size="Size.Large"
                       ButtonType="ButtonType.Submit"
                       FullWidth="true">
                <MudText>@L["Register"]</MudText>
            </MudButton>
        </EditForm>
    </div>
</div>

@code {
    public const string PageUrl = "/pages/authentication/externallogin";
    public const string LoginCallbackAction = "LoginCallback";

    private string? message;
    private ExternalLoginInfo externalLoginInfo = default!;

    [CascadingParameter] private HttpContext HttpContext { get; set; } = default!;

    [SupplyParameterFromForm] private InputModel Input { get; set; } = new();

    [SupplyParameterFromQuery] private string? RemoteError { get; set; }

    [SupplyParameterFromQuery] private string? ReturnUrl { get; set; }

    [SupplyParameterFromQuery] private string? Action { get; set; }

    private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName;

    protected override async Task OnInitializedAsync()
    {
        if (RemoteError is not null)
        {
            RedirectManager.RedirectToWithStatus(Login.PageUrl, $"Error from external provider: {RemoteError}", HttpContext);
        }

        var info = await SignInManager.GetExternalLoginInfoAsync();
        if (info is null)
        {
            RedirectManager.RedirectToWithStatus(Login.PageUrl, "Error loading external login information.", HttpContext);
        }

        externalLoginInfo = info;

        if (HttpMethods.IsGet(HttpContext.Request.Method))
        {
            if (Action == LoginCallbackAction)
            {
                await OnLoginCallbackAsync();
                return;
            }

            // We should only reach this page via the login callback, so redirect back to
            // the login page if we get here some other way.
            RedirectManager.RedirectTo(Login.PageUrl);
        }
    }

    private async Task OnLoginCallbackAsync()
    {
        // Sign in the user with this external login provider if the user already has a login.
        var result = await SignInManager.ExternalLoginSignInAsync(
            externalLoginInfo.LoginProvider,
            externalLoginInfo.ProviderKey,
            false,
            false);

        if (result.Succeeded)
        {
            Logger.LogInformation("{UserName} successfully logged in using {LoginProvider} external provider.",
                externalLoginInfo.Principal.Identity?.Name ?? "Unknown User",
                externalLoginInfo.LoginProvider);
            RedirectManager.RedirectTo(ReturnUrl);
        }
        else if (result.IsLockedOut)
        {
            Logger.LogWarning("{UserName} is locked out during login attempt with {LoginProvider} external provider.",
                externalLoginInfo.Principal.Identity?.Name ?? "Unknown User",
                externalLoginInfo.LoginProvider);
            RedirectManager.RedirectTo(Lockout.PageUrl);
        }

        // If the user does not have an account, then ask the user to create an account.
        if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
        }
    }

    private async Task OnValidSubmitAsync()
    {
        var emailStore = GetEmailStore();
        var user = CreateUser();
        user.TenantId=Input.TenantId;
        user.TimeZoneId=Input.TimeZoneId;
        user.LanguageCode=Input.LanguageCode;
        await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
        user.Provider = externalLoginInfo.LoginProvider;
        var result = await UserManager.CreateAsync(user);
        if (!result.Succeeded)
        {
            message = $"{string.Join(" ", result.Errors.Select(error => error.Description))}";
            return;
        }

        result = await UserManager.AddLoginAsync(user, externalLoginInfo);
        if (!result.Succeeded)
        {
            message = $"{string.Join(" ", result.Errors.Select(error => error.Description))}";
            return;
        }

        var userId = await UserManager.GetUserIdAsync(user);
        result = await UserManager.AddToRoleAsync(user, RoleName.Basic);
        if (!result.Succeeded)
        {
            message = $"{string.Join(" ", result.Errors.Select(error => error.Description))}";
            return;
        }

        Logger.LogInformation("A new account was created for user '{UserName}' using the external login provider '{LoginProvider}'.",
                Input.Email,
                externalLoginInfo.LoginProvider);

        if (UserManager.Options.SignIn.RequireConfirmedEmail)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Navigation.GetUriWithQueryParameters(
                Navigation.ToAbsoluteUri(ConfirmEmail.PageUrl).AbsoluteUri,
                new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = "/" });
            await Mediator.Publish(new UserActivationNotification(callbackUrl, Input.Email, userId, Input.Email));
            RedirectManager.RedirectTo(RegisterConfirmation.PageUrl);
        }
        // If account confirmation is required, we need to show the link if we don't have a real email sender
        else if (UserManager.Options.SignIn.RequireConfirmedAccount)
        {
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = Navigation.GetUriWithQueryParameters(
                Navigation.ToAbsoluteUri(ConfirmEmail.PageUrl).AbsoluteUri,
                new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
            RedirectManager.RedirectTo(RegisterConfirmation.PageUrl, new Dictionary<string, object?> { ["email"] = Input.Email, ["EmailConfirmationLink"] = callbackUrl });
        }

        await SignInManager.SignInAsync(user, true, externalLoginInfo.LoginProvider);
        RedirectManager.RedirectTo(ReturnUrl);
    }

    private ApplicationUser CreateUser()
    {
        try
        {
            return Activator.CreateInstance<ApplicationUser>();
        }
        catch
        {
            throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
                                                $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
        }
    }

    private IUserEmailStore<ApplicationUser> GetEmailStore()
    {
        if (!UserManager.SupportsUserEmail)
        {
            throw new NotSupportedException(L["The default UI requires a user store with email support."]);
        }

        return (IUserEmailStore<ApplicationUser>)UserStore;
    }

    private sealed class InputModel
    {
        [Required] [EmailAddress] public string Email { get; set; } = "";
        [Required] public string TenantId { get; set; } = "";
        [Display(Name = "Time Zone")]
        public string? TimeZoneId { get; set; } = TimeZoneInfo.Local.Id;
        [Display(Name = "Language")]
        public string? LanguageCode { get; set; } = CultureInfo.CurrentCulture.Name;
    }

}